<?php

namespace Modules\Wechat\Model\Data\OfficialAccount;

use Base\Exception\Data as Exception;
use Modules\Wechat\Model\Dao\Db\WcOaMenu;
use Modules\Wechat\Model\Data\Abstraction as DataAbstraction;

/**
 * 公众号菜单
 *
 * @package Modules\Wechat\Modal\Data\OfficialAccount
 */
class Menu extends DataAbstraction
{

    const DICT_SCOPE_TYPE = 'type';

    const TYPE_CLICK              = 'click';
    const TYPE_VIEW               = 'view';
    const TYPE_SCANCODE_PUSH      = 'scancode_push';
    const TYPE_SCANCODE_WAITMSG   = 'scancode_waitmsg';
    const TYPE_PIC_SYSPHOTO       = 'pic_sysphoto';
    const TYPE_PIC_PHOTO_OR_ALBUM = 'pic_photo_or_album';
    const TYPE_PIC_WEIXIN         = 'pic_weixin';
    const TYPE_LOCATION_SELECT    = 'location_select';
    const TYPE_MEDIA_ID           = 'media_id';
    const TYPE_VIEW_LIMITED       = 'view_limited';

    const MAX_MENU_LENGTH        = 3;
    const MAX_SUB_MENU_LENGTH    = 5;
    const MAX_MENU_NAME_BYTE     = 16;
    const MAX_SUB_MENU_NAME_BYTE = 40;

    const MAX_KEY_BYTE = 128;
    const MAX_URL_BYTE = 1024;

    protected static $dictionaries = [
        self::DICT_SCOPE_TYPE => [
            [self::DICT_ENTRY_ID => self::TYPE_CLICK, self::DICT_ENTRY_VALUE => '点击推事件'],
            [self::DICT_ENTRY_ID => self::TYPE_VIEW, self::DICT_ENTRY_VALUE => '跳转URL'],
            [self::DICT_ENTRY_ID => self::TYPE_SCANCODE_PUSH, self::DICT_ENTRY_VALUE => '扫码推事件'],
            [self::DICT_ENTRY_ID => self::TYPE_SCANCODE_WAITMSG, self::DICT_ENTRY_VALUE => '扫码推事件且弹出“消息接收中”提示框'],
            [self::DICT_ENTRY_ID => self::TYPE_PIC_SYSPHOTO, self::DICT_ENTRY_VALUE => '弹出系统拍照发图'],
            [self::DICT_ENTRY_ID => self::TYPE_PIC_PHOTO_OR_ALBUM, self::DICT_ENTRY_VALUE => '弹出拍照或者相册发图'],
            [self::DICT_ENTRY_ID => self::TYPE_PIC_WEIXIN, self::DICT_ENTRY_VALUE => '弹出微信相册发图器'],
            [self::DICT_ENTRY_ID => self::TYPE_LOCATION_SELECT, self::DICT_ENTRY_VALUE => '弹出地理位置选择器'],
            [self::DICT_ENTRY_ID => self::TYPE_MEDIA_ID, self::DICT_ENTRY_VALUE => '下发消息（除文本消息）'],
            [self::DICT_ENTRY_ID => self::TYPE_VIEW_LIMITED, self::DICT_ENTRY_VALUE => '跳转图文消息URL'],
        ],
    ];

    protected function init()
    {
        return $this;
    }

    /**
     * 获取菜单类型列表
     *
     * @return array
     */
    public function getTypeList()
    {
        return static::dictGetList(static::DICT_SCOPE_TYPE, []);
    }

    /**
     * 获取菜单类型名称
     *
     * @param string $id 菜单类型ID
     * @param string $default
     *
     * @return string
     */
    public function getTypeName($id, $default = '')
    {
        return static::dictGetEntryNameById(static::DICT_SCOPE_TYPE, $id, $default);
    }

    /**
     * 添加一级菜单
     *
     * @param string $account  菜单所属微信账号
     * @param string $type     菜单类型
     * @param string $name     菜单名称
     * @param string $key      菜单KEY值:click等点击类型必须,用于消息接口推送,不超过128字节.
     * @param string $url      网页链接:view类型必须,用户点击菜单可打开链接,不超过1024字节.
     * @param string $media_id 调用新增永久素材接口返回的合法media_id:media_id类型和view_limited类型必须
     * @param int    $sequence 顺序：值越小越靠左或靠上
     * @param int    $status   菜单状态:这个值决定是将此菜单数据是否用于更新公众号的菜单
     *
     * @return int
     * @author MoozieSong <xuehai.song@beebank.com>
     */
    public function add(
        $account,
        $type,
        $name,
        $key = '',
        $url = '',
        $media_id = '',
        $sequence = 0,
        $status = self::STATUS_NORMAL
    ) {
        return $this->save($account, $type, $name, $key, $url, $media_id, 0, $status, $sequence, 0);
    }


    /**
     * 添加二级菜单
     *
     * @param string $account   菜单所属微信账号
     * @param int    $parent_id 父菜单ID:为0时创建一级菜单
     * @param string $type      菜单类型
     * @param string $name      菜单名称
     * @param string $key       菜单KEY值:click等点击类型必须,用于消息接口推送,不超过128字节.
     * @param string $url       网页链接:view类型必须,用户点击菜单可打开链接,不超过1024字节.
     * @param string $media_id  调用新增永久素材接口返回的合法media_id:media_id类型和view_limited类型必须
     * @param int    $sequence  顺序：值越小越靠左或靠上
     * @param int    $status    菜单状态:这个值决定是将此菜单数据是否用于更新公众号的菜单
     *
     * @return int
     */
    public function addSub(
        $account,
        $parent_id,
        $type,
        $name,
        $key = '',
        $url = '',
        $media_id = '',
        $sequence = 0,
        $status = self::STATUS_NORMAL
    ) {
        return $this->save($account, $type, $name, $key, $url, $media_id, $parent_id, $status, $sequence, 0);
    }

    /**
     * 获取菜单数量
     *
     * @param string $account   菜单所属微信账号
     * @param int    $parent_id 父菜单ID:为0时查询一级菜单数量
     * @param int    $status    菜单状态
     *
     * @return int
     */
    public function count($account, $parent_id = 0, $status = self::STATUS_NORMAL)
    {
        $M     = new WcOaMenu();
        $M->oa = trim($account);
        if (!is_null($parent_id)) {
            $M->parent_id = intval($parent_id);
        }
        if (static::STATUS_ANY !== $status) {
            $M->status = intval($status);
        }
        return $M->count() ?: 0;
    }

    /**
     * 通过菜单在数据库中的ID更新菜单数据
     *
     * 不需要更新的传入空字符串.
     *
     * @param int        $id        菜单在数据库中的ID
     * @param string     $account   菜单所属微信账号:必传
     * @param string     $type      菜单类型:必传
     * @param string     $name      菜单名称
     * @param string     $key       菜单KEY值:click等点击类型必须,用于消息接口推送,不超过128字节.
     * @param string     $url       网页链接:view类型必须,用户点击菜单可打开链接,不超过1024字节.
     * @param string     $media_id  调用新增永久素材接口返回的合法media_id:media_id类型和view_limited类型必须
     * @param string|int $parent_id 父菜单ID:为空字符串时不更新,为0时将菜单改为一级菜单,为其它菜单的ID时将变为其子菜单
     * @param int        $sequence  顺序:值越小越靠左或靠上
     *
     * @param int        $status
     *
     * @return int
     */
    public function saveById(
        $id,
        $account,
        $type,
        $name = '',
        $key = '',
        $url = '',
        $media_id = '',
        $parent_id = '',
        $sequence = 0,
        $status = self::STATUS_NORMAL
    ) {
        return $this->save($account, $type, $name, $key, $url, $media_id, $parent_id, $status, $sequence, $id);
    }

    /**
     * 通过微信账号获取其菜单
     *
     * @param string $account     菜单所属微信账号
     * @param bool   $arrange_sub 整理子菜单(按钮):为true时会在一级菜单中增加'sub'字段,其中是子菜单列表.
     *
     * @return array|string
     */
    public function getByAccount($account, $arrange_sub = true)
    {
        return $this->read(['oa' => $account], $arrange_sub);
    }

    /**
     * 通过账户获取发布菜单需要的菜单数据
     *
     * @param string $account    菜单所属公众号的微信号
     * @param bool   $check_data 是否检查数据格式
     *
     * @return array
     * @throws \Base\Exception\Data
     */
    public function getByAccountForRelease($account, $check_data = false)
    {
        if ($list = $this->read(['oa' => $account, 'status' => static::STATUS_NORMAL], true)) {
            $data = [];
            foreach ($list as $i => $menu) {
                $has_sub = !empty($menu['sub']);
                if ($check_data && (!$menu['type'] && !$has_sub)) {
                    throw new Exception('error.admin_wechat.menu.sub_empty');
                }
                $data[$i] = $this->processDataForRelease($menu);
                if ($has_sub) {
                    foreach ($menu['sub'] as $sub) {
                        $data[$i]['sub_button'][] = $this->processDataForRelease($sub);
                    }
                }
            }
            if (static::MAX_MENU_LENGTH > count($data)) {
                throw new Exception('error.admin_wechat.menu.too_little');
            }
            return array_values($data);
        }
        return [];
    }

    /**
     * 通过微信号获取其素有一级菜单
     *
     * @param string $account 菜单所属公众号的微信号
     *
     * @return array
     */
    public function getTopByAccount($account)
    {
        return $this->read(['oa' => $account, 'parent_id' => 0]);
    }

    /**
     * 通过菜单在数据库中的ID获取菜单详情
     *
     * @param int  $id       菜单在数据库中的ID
     * @param bool $with_sub 是否返回子菜单
     *
     * @return array
     */
    public function getById($id, $with_sub = true)
    {
        if ($menu = $this->read(['id' => $id], false)) {
            $menu['sub'] = [];
            if ($with_sub) {
                $menu['sub'] = $this->read(['parent_id' => $id], false) ?: [];
            }
            return $menu;
        }
        return [];
    }

    /**
     * 通过菜单在数据库中的ID删除菜单
     *
     * @param int  $id   菜单在数据库中的ID
     * @param bool $hard 是否硬删除
     *
     * @return bool|int
     */
    public function deleteById($id, $hard = false)
    {
        $M = new WcOaMenu();
        return $hard
            ? $M->deleteByPk($id)
            : $M->setProperty('status', static::STATUS_DELETED)->updateByPk();
    }


    /**
     * 检查菜单个数
     *
     * 微信规定:一级菜单不超过3个,每个一级菜单的子菜单(二级菜单)不超过5个.
     *
     * @param string $account   菜单所属微信账号
     * @param int    $parent_id 父菜单ID:为0时检查一级菜单数量
     * @param bool   $is_update 是否是更新
     *
     * @return $this
     * @throws Exception
     */
    protected function checkLength($account, $parent_id = 0, $is_update = false)
    {
        if ($parent_id) {
            if (!$is_update && static::MAX_SUB_MENU_LENGTH <= $this->count($account,
                                                                           $parent_id,
                                                                           static::STATUS_NORMAL)
            ) {
                throw new Exception('error.admin_wechat.menu.too_much_sub');
            }
        } else {
            if (!$is_update && static::MAX_MENU_LENGTH <= $this->count($account, 0, static::STATUS_NORMAL)) {
                throw new Exception('error.admin_wechat.menu.too_much');
            }
        }
        return $this;
    }

    /**
     * 检查菜单类型及类型对应的参数
     *
     * @param string $type     菜单类型
     * @param string $key      菜单KEY值:click等点击类型必须,用于消息接口推送,不超过128字节.
     * @param string $url      网页链接:view类型必须,用户点击菜单可打开链接,不超过1024字节.
     * @param string $media_id 调用新增永久素材接口返回的合法media_id:media_id类型和view_limited类型必须
     *
     * @return $this
     * @throws Exception
     */
    protected function checkType($type, $key = '', $url = '', $media_id = '')
    {
        if (empty($type) && ($key || $url || $media_id)) {
            throw new Exception('一级菜单不能有Key,URL,媒体ID(因为它只负责显示其子菜单,设置了也没用)');
        }
        switch ($type) {
            case static::TYPE_CLICK:
                if (empty($key)) {
                    throw new Exception('error.admin_wechat.menu.click_require_key');
                }
                if (static::MAX_KEY_BYTE < strlen($key)) {
                    throw new Exception('error.admin_wechat.menu.key_too_long');
                }
                break;
            case static::TYPE_VIEW:
                if (empty($url)) {
                    throw new Exception('error.admin_wechat.menu.view_require_url');
                }
                if (static::MAX_URL_BYTE < strlen($url)) {
                    throw new Exception('error.admin_wechat.menu.url_too_long');
                }
                break;
            case static::TYPE_MEDIA_ID:
            case static::TYPE_VIEW_LIMITED:
                if (empty($media_id)) {
                    throw new Exception('error.admin_wechat.menu.require_media_id');
                }
                break;
        }
        return $this;
    }

    /**
     * 检查父菜单
     *
     * @param int $parent_id 父菜单ID
     * @param int $self_id
     *
     * @return $this|bool
     * @throws Exception
     */
    protected function checkParent($parent_id, $self_id = 0)
    {
        if (!$parent_id) {
            return true;
        }
        if (!is_numeric($parent_id) || $parent_id < 0) {
            throw new Exception('父菜单ID错误');
        }
        if (!(new WcOaMenu)->setProperty('id', $parent_id)->count()) {
            throw new Exception('父菜单不存在');
        }
        if ($self_id && $parent_id && $parent_id == $self_id) {
            throw new Exception('父菜单不能是自己');
        }
        return $this;
    }

    /**
     * 当前菜单是否有子菜单
     *
     * @param int $id 当前菜单ID
     * @param int $sub_status
     *
     * @return int 子菜单数量
     */
    protected function hasSub($id, $sub_status = self::STATUS_NORMAL)
    {
        $properties = [
            'status'    => $sub_status,
            'parent_id' => $id,
        ];
        return (new WcOaMenu)->setProperties($properties)->count();
    }

    /**
     * 新建或按主键更新菜单
     *
     * @param string $account   菜单所属微信账号
     * @param string $type      菜单类型
     * @param string $name      菜单名称
     * @param string $key       菜单KEY值:click等点击类型必须,用于消息接口推送,不超过128字节.
     * @param string $url       网页链接:view类型必须,用户点击菜单可打开链接,不超过1024字节.
     * @param string $media_id  调用新增永久素材接口返回的合法media_id:media_id类型和view_limited类型必须
     * @param int    $parent_id 父菜单ID:为0时创建一级菜单
     * @param int    $status    菜单状态:这个值决定是将此菜单数据是否用于更新公众号的菜单
     * @param int    $sequence  顺序:数字越小越靠左或靠上
     *
     * @param int    $id        菜单在数据库中的ID:为0时新建,否则按ID更新
     *
     * @return int
     * @throws Exception
     */
    protected function save(
        $account,
        $type,
        $name,
        $key = null,
        $url = null,
        $media_id = null,
        $parent_id = null,
        $status = self::STATUS_NORMAL,
        $sequence = 0,
        $id = 0
    ) {
        if ($this->checkLength($account, $parent_id, $id > 0)
                 ->checkType($type, $key, $url, $media_id)
                 ->checkParent($parent_id, $id)
        ) {
            if (empty($name)) {
                throw new Exception('error.admin_wechat.menu.name_empty');
            }
            if ($id) {
                if ($exists = $this->getById($id)) {
                    if ($exists['parent_id']) {
                        if (static::MAX_SUB_MENU_LENGTH <= $this->count($account, $exists['parent_id'])) {
                            throw new Exception('error.admin_wechat.menu.too_much_sub');
                        }
                    }
                }
            }
            $Menu            = new WcOaMenu;
            $Menu->oa        = $account;
            $Menu->type      = $type;
            $Menu->name      = is_null($name) ? null : trim($name);
            $Menu->key       = is_null($key) ? null : trim($key);
            $Menu->url       = is_null($url) ? null : trim($url);
            $Menu->media_id  = is_null($media_id) ? null : trim($media_id);
            $Menu->parent_id = is_null($parent_id) ? null : intval($parent_id);
            $Menu->status    = is_null($status) ? null : intval($status);
            $Menu->sequence  = is_null($sequence) ? null : intval($sequence);
            return empty($id) ? $Menu->create(true) : $Menu->updateByPk($id);
        }
        return 0;
    }

    /**
     * 按条件更新
     *
     * @param array $updating
     * @param array $conditions
     *
     * @return bool|int
     */
    protected function update(array $updating, array $conditions)
    {
        if (empty($conditions) || empty($updating)) {
            return 0;
        }
        return (new WcOaMenu)->setProperties($updating)->update($conditions);
    }

    /**
     * 按条件读取
     *
     * @param array $condition   条件
     * @param bool  $arrange_sub 整理子菜单(按钮):为true时会在一级菜单中增加'sub'字段,其中是子菜单列表.
     *
     * @return array|string
     */
    protected function read(array $condition, $arrange_sub = true)
    {
        if ($list = (new WcOaMenu)->setProperties($condition)->read([], ['parent_id' => 'ASC', 'sequence' => 'ASC'])) {
            foreach ($list as &$data) {
                $data = $this->processDetail($data);
            }
            if ($arrange_sub) {
                $arranged = [];
                foreach ($list as $menu) {
                    if ($menu['parent_id']) {
                        $arranged[$menu['parent_id']]['sub'][] = $menu;
                    } else {
                        $arranged[$menu['id']] = $menu;
                    }
                }
                return $arranged;
            }
        }
        return $list;
    }

    /**
     * 菜单是否已删除
     *
     * 本地数据库中的菜单是否已删除,不是线上的.
     *
     * @param int $status 本地菜单的数据状态
     *
     * @return bool
     */
    protected static function isDeleted($status)
    {
        return is_numeric($status) && static::STATUS_DELETED == $status;
    }

    /**
     * 处理菜单数据
     *
     * 在获取菜单列表或单个菜单的详情时,通过这个方法可以补全一些常用数据.
     *
     * @param array $data 菜单数据
     *
     * @return array
     */
    protected function processDetail(array $data)
    {
        $data['status_name'] = $this->getStatusName($data['status']);
        $data['type_name']   = $this->getTypeName($data['type'], '仅按钮');
        $data['level']       = $data['parent_id'] ? 2 : 1;
        $data['level_name']  = 1 == $data['level'] ? '一级菜单' : '二级菜单';
        $data['is_deleted']  = static::isDeleted($data['status']);
        $data['sub']         = [];
        return $data;
    }

    /**
     * 将本地菜单数据格式化为发布到线上需要的格式
     *
     * @param array $data 本地菜单数据
     *
     * @return array
     */
    protected function processDataForRelease(array $data)
    {
        $button['name'] = $data['name'];
        if ($data['type']) {
            $button['type'] = $data['type'];
            switch ($data['type']) {
                case static::TYPE_MEDIA_ID:
                case static::TYPE_VIEW_LIMITED:
                    $button['media_id'] = $data['media_id'];
                    break;
                case static::TYPE_VIEW:
                    $button['url'] = $data['url'];
                    break;
                default:
                    $button['key'] = $data['key'];
            }
        } else {
            $button['sub_button'] = [];
        }
        return $button;
    }

    /**
     * 按条件删除
     *
     * @param array $conditions 条件
     * @param bool  $hard       是否硬删除
     *
     * @return bool|int
     */
    protected function delete(array $conditions, $hard = false)
    {
        $Menu = new WcOaMenu();
        return $hard
            ? $Menu->setProperties($conditions)->delete(true)
            : $Menu->setProperty('status', static::STATUS_DELETED)->update($conditions, 0, true);
    }

}